package com.sdsmdg.harjot.materialshadows.shadowutils;

import com.sdsmdg.harjot.materialshadows.MaterialShadowViewWrapper;
import com.sdsmdg.harjot.materialshadows.utilities.GrahamScan;
import com.sdsmdg.harjot.materialshadows.utilities.Point2D;
import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.Image;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.PixelMapElement;
import ohos.agp.render.*;
import ohos.agp.utils.Color;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.image.PixelMap;
import ohos.media.image.common.*;
import ohos.utils.PlainArray;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static com.sdsmdg.harjot.materialshadows.Constants.*;

/**
 * 阴影生成器
 *
 * @since 2021-05-09
 */
public class ShadowGenerator {
    private static final HiLogLabel hiLogLabel = new HiLogLabel(HiLog.LOG_APP,0x22,"ShadowGenerator");
    private static final int MASKFILTER_RADIUS = 30;
    private static final float PAINT_ALPHA = 0.16f;

    private final Object TASKS_LOCK = new Object();

    private final List<Future<?>> tasksInProgress = new ArrayList<>();
    private final ExecutorService workerPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    private final EventHandler uiThreadHandler = new EventHandler(EventRunner.getMainEventRunner());

    private PlainArray<Path> viewPaths;

    private float offsetX = DEFAULT_X_OFFSET;
    private float offsetY = DEFAULT_Y_OFFSET;

    private float shadowAlpha = DEFAULT_SHADOW_ALPHA;

    private boolean shouldShowWhenAllReady = DEFAULT_SHOW_WHEN_ALL_READY;
    private boolean shouldCalculateAsync = DEFAULT_CALCULATE_ASYNC;
    private boolean shouldAnimateShadow = DEFAULT_ANIMATE_SHADOW;

    private int animationDuration = DEFAULT_ANIMATION_TIME;

    private int childrenWithShadow;

    private final ComponentContainer viewGroup;
    private Element mElement;
    private Element mCalculateElement;
    private final Paint mBgPaint = new Paint();

    private AnimatorValue animatorValue;

    /**
     * 初始化
     *
     * @param viewGroup viewGroup
     * @param offsetX offsetX
     * @param offsetY offsetY
     * @param shadowAlpha shadowAlpha
     * @param shouldAnimateShadow shouldAnimateShadow
     * @param shouldCalculateAsync shouldCalculateAsync
     * @param shouldShowWhenAllReady shouldShowWhenAllReady
     * @param animationDuration animationDuration
     */
    public ShadowGenerator(ComponentContainer viewGroup, float offsetX,
                           float offsetY, float shadowAlpha, boolean shouldShowWhenAllReady,
                           boolean shouldCalculateAsync, boolean shouldAnimateShadow, int animationDuration) {
        this.viewGroup = viewGroup;
        this.offsetX = offsetX;
        this.offsetY = offsetY;
        this.shadowAlpha = shadowAlpha;
        this.shouldShowWhenAllReady = shouldShowWhenAllReady;
        this.shouldCalculateAsync = shouldCalculateAsync;
        this.shouldAnimateShadow = shouldAnimateShadow;
        this.animationDuration = animationDuration;
        initPaint();
    }

    public float getOffsetX() {
        return offsetX;
    }

    /**
     * X轴偏移量
     *
     * @param offsetX X轴偏移量
     */
    public void setOffsetX(float offsetX) {
        this.offsetX = offsetX;
        updateShadows(POS_UPDATE_ALL);
    }

    /**
     * 得到背景
     *
     * @return Element
     */
    public Element getmElement() {
        return mElement;
    }

    /**
     * 设置背景
     *
     * @param element 背景
     */
    public void setmElement(Element element) {
        this.mElement = element;
    }

    /**
     * 得到计算后的背景
     *
     * @return Element
     */
    public Element getmCalculateElement() {
        return mCalculateElement;
    }

    /**
     * 设置计算后的背景
     *
     * @param calculateElement 计算后的背景
     */
    public void setmCalculateElement(Element calculateElement) {
        this.mCalculateElement = calculateElement;
    }

    /**
     * 得到Y轴偏移量
     *
     * @return offsetY
     */
    public float getOffsetY() {
        return offsetY;
    }

    /**
     * 设置Y轴偏移量
     *
     * @param offsetY 设置Y轴偏移量
     */
    public void setOffsetY(float offsetY) {
        this.offsetY = offsetY;
        updateShadows(POS_UPDATE_ALL);
    }

    /**
     * 得到透明度
     *
     * @return shadowAlpha
     */
    public float getShadowAlpha() {
        return shadowAlpha;
    }

    /**
     * 设置阴影透明度
     *
     * @param shadowAlpha 设置阴影透明度
     */
    public void setShadowAlpha(float shadowAlpha) {
        this.shadowAlpha = shadowAlpha;
        updateShadows(POS_UPDATE_ALL);
    }

    public boolean isShouldShowWhenAllReady() {
        return shouldShowWhenAllReady;
    }

    public void setShouldShowWhenAllReady(boolean shouldShowWhenAllReady) {
        this.shouldShowWhenAllReady = shouldShowWhenAllReady;
    }

    public boolean isShouldCalculateAsync() {
        return shouldCalculateAsync;
    }

    public void setShouldCalculateAsync(boolean shouldCalculateAsync) {
        this.shouldCalculateAsync = shouldCalculateAsync;
    }

    public boolean isShouldAnimateShadow() {
        return shouldAnimateShadow;
    }

    public void setShouldAnimateShadow(boolean shouldAnimateShadow) {
        this.shouldAnimateShadow = shouldAnimateShadow;
    }

    public int getAnimationDuration() {
        return animationDuration;
    }

    /**
     * 设置动画时间
     *
     * @param animationDuration 设置动画时间
     */
    public void setAnimationDuration(int animationDuration) {
        this.animationDuration = animationDuration;
    }

    /**
     * 产生
     */
    public void generate() {
        clearShadowCache(); // Maybe some children changed their size
        childrenWithShadow = 0;
        for (int i1 = 0; i1 < viewGroup.getChildCount(); i1++) {
            Component view = viewGroup.getComponentAt(i1);
            if (view instanceof MaterialShadowViewWrapper) {
                continue;
            }
            view.setClipEnabled(false);
            childrenWithShadow++;
            if (shouldCalculateAsync) {
                calculateAndRenderShadowAsync(view, i1);
            } else {
                calculateAndRenderShadow(view, i1);
            }
        }
    }

    private void initPaint() {
        mBgPaint.setColor(new Color(Color.getIntColor("#26000000")));
        mBgPaint.setMaskFilter(new MaskFilter(MASKFILTER_RADIUS, MaskFilter.Blur.NORMAL));
        mBgPaint.setAntiAlias(true);
        mBgPaint.setDither(true);
        mBgPaint.setAlpha(0f);
    }

    /**
     * 释放资源
     */
    public void releaseResources() {
        workerPool.shutdown();
        uiThreadHandler.removeEvent(0, null);
    }

    private void clearShadowCache() {
        cancelTasksInProgress();
        uiThreadHandler.removeEvent(0, null);
        viewPaths = new PlainArray<>();
    }

    private void updateShadows(int pos) {
        if (pos == POS_UPDATE_ALL) {
            for (int i1 = 0; i1 < viewGroup.getChildCount(); i1++) {
                setShadowOutlineProviderAt(i1);
            }
        } else {
            setShadowOutlineProviderAt(pos);
        }
        if (shouldAnimateShadow) {
            animateOutlineAlpha();
        }
    }

    private void setShadowOutlineProviderAt(int childIndex) {
        Path shadowPath = getViewPathWithOffsetAt(childIndex);
        if (shadowPath == null) {
            // Path calculation is still in progress
            return;
        }
        final Component child = viewGroup.getComponentAt(childIndex);
        PixelMap pixelMap = null;
        if (child instanceof Image) {
            pixelMap = ((Image) child).getPixelMap();
        } else {
            Element element = child.getBackgroundElement();
            if (element == null) {
                element = child.getForegroundElement();
            }
            if (element instanceof PixelMapElement) {
                pixelMap = ((PixelMapElement) element).getPixelMap();
            }
        }

        float[] scaleValueArray = calculateScaleValue(child, pixelMap);

        child.addDrawTask(new Component.DrawTask() {
            @Override
            public void onDraw(Component component, Canvas canvas) {
                canvas.scale(scaleValueArray[0], scaleValueArray[1]);
                canvas.translate(offsetX + component.getPaddingLeft(), offsetY + component.getPaddingTop());
                canvas.drawPath(shadowPath, mBgPaint);
            }
        }, Component.DrawTask.BETWEEN_BACKGROUND_AND_CONTENT);
    }

    private void animateOutlineAlpha() {
        if (animatorValue == null) {
            animatorValue = new AnimatorValue();
            animatorValue.setDuration(animationDuration);
            animatorValue.setCurveType(Animator.CurveType.LINEAR);
            animatorValue.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
                @Override
                public void onUpdate(AnimatorValue animatorValue, float v) {
                    mBgPaint.setAlpha(PAINT_ALPHA * v);
                    for (int i1 = 0; i1 < viewGroup.getChildCount(); i1++) {
                        viewGroup.getComponentAt(i1).invalidate();
                    }
                }
            });
        }
        if (!animatorValue.isRunning()) {
            animatorValue.start();
        }
    }

    private Path getViewPathWithOffsetAt(int position) {
        Path path = new Path();
        if (position == POS_UPDATE_ALL) {
            Path noOffsetPath = viewPaths.get(position).get();

            if (null == noOffsetPath) {
                return null;
            }
            path.set(noOffsetPath);
        } else {
            Path path1;
            Optional<Path> noOffsetPath = viewPaths.get(position);

            if (noOffsetPath.isPresent()) {
                path1 = noOffsetPath.get();
                path.set(path1);
            }
        }

        return path;
    }

    private void calculateAndRenderShadowAsync(final Component view, final int pos) {
        final Future[] future = new Future[1];
        future[0] = workerPool.submit(new Runnable() {
            @Override
            public void run() {
                calculateAndRenderShadow(view, pos);
                synchronized (TASKS_LOCK) {
                    tasksInProgress.remove(future[0]);
                }
            }
        });
        tasksInProgress.add(future[0]);
    }

    private void cancelTasksInProgress() {
        synchronized (TASKS_LOCK) {
            for (int i1 = tasksInProgress.size() - 1; i1 >= 0; i1--) {
                Future<?> task = tasksInProgress.get(i1);
                task.cancel(true);
                tasksInProgress.remove(i1);
            }
        }
    }

    private void calculateAndRenderShadow(final Component view, int pos) {
        if (Thread.currentThread().isInterrupted()) {
            return;
        }

        Path path = new Path();
        List<Point2D> arrayListOutlinePoints;

        PixelMap bitmap = null;
        if (view instanceof Image) {
            bitmap =  ((Image) view).getPixelMap();
        }
        if (bitmap == null) {
            bitmap = ((PixelMapElement) view.getBackgroundElement()).getPixelMap();
        }
        if (bitmap == null) {
            bitmap = ((PixelMapElement) view.getForegroundElement()).getPixelMap();
        }
        if (bitmap != null) {
            try {
                // We need to copy it, because drawing cache will be recycled if view is detached
                arrayListOutlinePoints = getOutlinePoints(bitmap);
                if (arrayListOutlinePoints.isEmpty()) {
                    return;
                }
            } catch (Exception e) {
                // If drawing cache has been recycled, IllegalStateException will be thrown on copy
                HiLog.error(hiLogLabel,"exception:" + e.getMessage());
                return;
            }
            Point2D[] outlinePoints = arrayListOutlinePoints.toArray(new Point2D[arrayListOutlinePoints.size()]);
            GrahamScan grahamScan = new GrahamScan(outlinePoints);
            List<Point2D> arrayListHullPoints = new ArrayList<>();
            for (Point2D point2D : grahamScan.hull()) {
                arrayListHullPoints.add(point2D);
            }
            Point2D[] hullPoints = arrayListHullPoints.toArray(new Point2D[arrayListHullPoints.size()]);
            path.moveTo((float) hullPoints[0].x(), (float) hullPoints[0].y());
            for (int i1 = 1; i1 < hullPoints.length; i1++) {
                path.lineTo((float) hullPoints[i1].x(), (float) hullPoints[i1].y());
            }
            synchronized (TASKS_LOCK) {
                if (Thread.currentThread().isInterrupted()) {
                    return;
                }
                uiThreadHandler.postSyncTask(new SetViewOutlineTask(pos, path));
            }
        }
    }

    private synchronized float[] calculateScaleValue(Component component, PixelMap pixelMap) {
        if (component != null && pixelMap != null) {
            int width = component.getWidth() - component.getPaddingRight();
            int height = component.getHeight() - component.getPaddingTop();
            Size size = pixelMap.getImageInfo().size;
            if (width > 0) {
                float size_x = width / (float) size.width;
                float size_y = height / (float) size.height;
                return new float[]{size_x, size_y};
            }
        }
        return new float[]{1.0f, 1.0f};
    }

    private List<Point2D> getOutlinePoints(PixelMap bitmap) {
        ArrayList<Point2D> arrayList = new ArrayList<>();

        Size size = bitmap.getImageInfo().size;

        for (int i1 = 0; i1 < size.height; i1++) {
            Position position = new Position();
            position.posX = 0;
            position.posY = i1;
            if (Color.alpha(bitmap.readPixel(position)) > 0) {
                arrayList.add(new Point2D(0, i1));
            }
            Position position1 = new Position();
            position1.posX = size.width - 1;
            position1.posY = i1;
            if (Color.alpha(bitmap.readPixel(position1)) > 0) {
                arrayList.add(new Point2D(size.width - 1, i1));
            }
        }

        if (Thread.currentThread().isInterrupted()) {
            return Collections.emptyList();
        }

        for (int i1 = 0; i1 < size.height; i1++) {
            for (int j1 = 1; j1 < size.width - 1; j1++) {
                Position position3 = new Position();
                position3.posX = j1 - 1;
                position3.posY = i1;
                Position position2 = new Position();
                position2.posX = j1;
                position2.posY = i1;
                if (Color.alpha(bitmap.readPixel(position3)) == 0 && Color.alpha(bitmap.readPixel(position2)) > 0) {
                    arrayList.add(new Point2D(j1, i1));
                }
                if (Color.alpha(bitmap.readPixel(position3)) > 0 && Color.alpha(bitmap.readPixel(position2)) == 0) {
                    arrayList.add(new Point2D(j1 - 1, i1));
                }
            }
        }
        return arrayList;
    }

    /**
     * 设置视图大纲任务
     *
     * @since 2021-05-09
     */
    private class SetViewOutlineTask implements Runnable {
        private int viewPos;
        private Path shadowPath;

        private SetViewOutlineTask(int pos, Path path) {
            this.viewPos = pos;
            this.shadowPath = path;
        }

        @Override
        public void run() {
            if (viewGroup.isBoundToWindow()) {
                viewPaths.put(viewPos, shadowPath);
                if (shouldShowWhenAllReady) {
                    if (viewPaths.size() == childrenWithShadow) {
                        updateShadows(POS_UPDATE_ALL);
                    }
                    return;
                }
                updateShadows(viewPos);
            }
        }
    }
}
